iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 26
0
Modern Web

一起挑戰 JavaScript 30 吧!系列 第 26

JS30 Day 26 - Stripe Follow Along Nav

  • 分享至 

  • xImage
  •  

成品連結:Stripe Follow Along Nav操作前程式碼完成後程式碼

今天要做的作品我覺得還蠻有趣的,JS30 作者的靈感是這個網站的導覽列切換的方式,如果只是切換 display: nonedisplay: block 是做不出這種效果的,今天我們就一起學習做做看吧!

背景是怎麼做到動態調整大小的?

當從一個選項移動到另一個時,白色背景看起來就像是動態地調整大小,這所用到的方法其實與第 22 天的方式相同,就是透過 element.getBoundingClientRect() 取得寬、高以及位置所做到

解釋完做法,我們就開始吧!

設定監聽事件

我們想要的效果是當滑鼠移入導覽列選項時時顯示 dropdown、當移出時隱藏,故要設定兩種監聽事件分別處理不同狀況

// 會使用到的 DOM 元素
const triggers = document.querySelectorAll('.cool > li');
const background = document.querySelector('.dropdownBackground');
const nav = document.querySelector('.top');

triggers.forEach(li => li.addEventListener('mouseenter', showDropdown));
triggers.forEach(li => li.addEventListener('mouseleave', hideDropdown));

display: none --> display: block 加入動畫效果?

當必需使用 display: none 又希望更改成 display: block時能加入 transition 的動畫效果,但是 trnasition 並不支援 display 的顯現與隱藏,這時就需要用一點小方法了。

由於不能直接在 displaytransition(會沒有效果),所以要利用 CSS 的 opacity 以及 setTimeout 來完成(opacity 可以是其他屬性,像是 max-widthvisibility 等等...)。

依順序理解如下:

  1. display: none 轉成 display: block
  2. 設定 setTimeout,並在 callback 中更改 opacity: 1 與配合設定的 transition 讓元素出現

綜合以上解釋,我們來處理我們的 navbar 吧!

function showDropdown(e) {
    this.classList.add('trigger-enter');
    setTimeout(() => {
        this.classList.add('trigger-enter-active');
    }, 150);
}

方法如上面所解釋,我們先在導覽列選項新增 class trigger-enter,更改 dropdown 的 CSS display: block,並在 150 毫秒後新增另一個 class trigger-enter-active 將透明度改為 1,如此動畫效果就出來了!(當然還是要配合 transition

this ?

this.classList.add('trigger-enter')this 很明顯是被觸發事件的元素,但是 setTimeout 裡面的 this 呢?

當你不是使用 arrow function 時會發現 thiswindow 而不是如上被觸發事件的元素。這是因為 function 的 this 並不會繼承,即便 showDropdownthis 是被觸發事件的元素,當你在裡面寫入新的 function 時,this 會變成 window 而不原本的元素。有個 hack 是在外面先將 this 存入變數,在裡面的 function 使用變數而不是 this

在我們狀況下會是這樣:

function showDropdown(e) {
    this.classList.add('trigger-enter');
    
    const self = this;
    setTimeout(function() {
        self.classList.add('trigger-enter-active');
    }, 150);
}

可以看到我們將 this 從入變數 self,這樣使用上就沒有問題了!

另一個方法是使用 ES6 的 arrow function,它的 this 會與包著他的function 相同,所以不用另外存入變數中,但細節這裡就不多說了。

mouseleave 時移除 class

showDropdown 先設定一些東西了,接下來當滑鼠移出時要隱藏 dropdown 內容。這個簡單許多,只要將 class 移除即可

function hideDropdown(e) {
    this.classList.remove('trigger-enter', 'trigger-enter-active');
  }

製作白色背景

製作白色背景大家應該不陌生,就是使用之前使用過的element.getBoundingClientRect() 以取得 dropdown 的寬、高以及位置

照著之前的方式,我們要先取得 dropdown 的資訊並套用到 background

function showDropdown(e) {
    this.classList.add('trigger-enter');
    
    const self = this;
    setTimeout(function() {
        self.classList.add('trigger-enter-active');
    }, 150);
    
    const dropdownCoords = dropdown.getBoundingClientRect();
    const coords = {
        width: dropdownCoords.width,
        height: dropdownCoords.height,
        top: dropdownCoords.top,
        left: dropdownCoords.left
    }
    
    background.classList.add('open');
    background.style.width = `${coords.width}px`;
    background.style.height = `${coords.height}px`;
    background.style.transform = `translate(${coords.left}px, ${coords.top}px)`;
}

與之前唯一不同是要新增 class open,這讓 background 的透明度變為 1,並配合 transition 呈現動畫效果

滑鼠移入設定好了,接著設定 hideDropdown

function hideDropdown(e) {
    this.classList.remove('trigger-enter', 'trigger-enter-active');
    background.classList.remove('open');
  }

白色背景好像太下面了?

你沒看錯,當初這個問題也困擾我很久,我一直找不到發生的原因。

這裡先說明一下 element.getBoundingClientRect() 的寬、高沒有問題,而 topbottomleftright 是與 viewport(使用者裝置視窗)上下左右的距離。以 top 來說距離是從瀏覽器上緣到導覽列選項頂端的距離。

但是當套用到 background 時,top 的起點是 nav 上緣而不是瀏覽器上緣,這也是為什麼 background 的位置會這麼下面了

可以試試看將 nav 上方的 p 以及 h2 移除,會發現這個問題消失了,因為現在 nav 上緣就是瀏覽器上緣

解法是要扣掉 nav 頂端到瀏覽器上緣的距離,才時 background 應該有的 top

function showDropdown(e) {
    this.classList.add('trigger-enter');
    
    const self = this;
    setTimeout(function() {
        self.classList.add('trigger-enter-active');
    }, 150);
    
    const dropdownCoords = dropdown.getBoundingClientRect();
    const navCoords = nav.getBoundingClientRect();
    const coords = {
        width: dropdownCoords.width,
        height: dropdownCoords.height,
        top: dropdownCoords.top - navCoords.top, // 扣掉 nav 到頂端的距離
        left: dropdownCoords.left - navCoords.left // 扣掉 nav 到左側的距離
    }
    
    background.classList.add('open');
    background.style.width = `${coords.width}px`;
    background.style.height = `${coords.height}px`;
    background.style.transform = `translate(${coords.left}px, ${coords.top}px)`;
}

到這裡算是完成了,但如果看仔細點會發現當你快速在三個導覽列選項移動時,trigger-enter-active 這個 class 並不會因滑鼠移出而移除。這是因為移動速度過快,在 setTimeout 150 毫秒加入 class 前就試著刪除 class,也因此會殘留。
解決變法是在 setTimeout 的 callback 中加入判斷式,當有 class trigger-enter 時才執行 setTimeout

setTimeout(() => {
    if (this.classList.contains('trigger-enter')) {
        this.classList.add('trigger-enter-active');
    }
}, 150);

或是可以更精簡的寫成

setTimeout(() => this.classList.contains('trigger-enter') && this.classList.add('trigger-enter-active'), 150);

當 && 左方為 true 時才會執行右方動作

完成!

Reference


上一篇
JS30 Day 25 - Event Capture, Propagation, Bubbling and Once
下一篇
JS30 Day 27 - Click and Drag
系列文
一起挑戰 JavaScript 30 吧!30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言